package cn.org.donald.framework.interceptor;

import cn.org.donald.framework.config.ServiceThreadPool;
import cn.org.donald.framework.util.ReflectUtil;
import cn.org.donald.pojo.entity.system.LogSql;
import cn.org.donald.service.system.LogSqlService;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * mapper拦截采集日志
 *
 * @author: Donald
 * @since: 2021-01-08 21:32:31
 **/

@Component
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "query",
        args = {Statement.class, ResultHandler.class}
), @Signature(
        type = StatementHandler.class,
        method = "update",
        args = {Statement.class}
), @Signature(
        type = StatementHandler.class,
        method = "batch",
        args = {Statement.class}
)})
public class SqlStatementInterceptor implements Interceptor {

    private static Logger logger = LoggerFactory.getLogger(SqlStatementInterceptor.class);

    private static final String EXCEPT_SERVER = "donald-provider-log";

    private Method druidGetSQLMethod;

    private Method oracleGetOriginalSqlMethod;

    private Statement statement;

    @Value("${spring.application.name}")
    private String serverName;

    @Reference(version = "1.0.0", group = "donald")
    private LogSqlService logSqlService;

    @Resource
    private ServiceThreadPool serviceThreadPool;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (EXCEPT_SERVER.equals(serverName)) {
            return invocation.proceed();
        }


        Object firstArg = invocation.getArgs()[0];
        this.statement = (Statement) (Proxy.isProxyClass(firstArg.getClass())
                ? SystemMetaObject.forObject(firstArg).getValue("h.statement")
                : firstArg);

        MetaObject stmtMetaObj = SystemMetaObject.forObject(this.statement);

        try {
            this.statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        } catch (Exception e) {
            logger.debug("Get value from MetaObject fail , the key is : {} !", "stmt.statement");
        }

        if (stmtMetaObj.hasGetter("delegate")) {
            try {
                this.statement = (Statement) stmtMetaObj.getValue("delegate");
            } catch (Exception var19) {
                logger.debug("Get value from MetaObject fail , the key is : {} !", "delegate");
            }
        }

        // get sql
        String originalSql = null;
        try {
            originalSql = this.getOriginalSql();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (originalSql == null) {
            originalSql = this.statement.toString();
        }
        originalSql = originalSql.replaceAll("[\\s]+", " ");// 正则表达式中\s匹配任何空白字符，包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]

        int index = this.indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }

        long startDate = System.currentTimeMillis();
        try {
            return invocation.proceed();// 执行sql的过程
        } finally {
            LogSql logSql = new LogSql();
            logSql.setMethod(this.getStatementId(invocation));
            logSql.setTimeConsume(System.currentTimeMillis() - startDate);
            logSql.setSqlStatement(originalSql);
            logSql.setApplication(serverName);
            logger.debug("Is going to write log into database ...");
            try {
                Future<?> submit = serviceThreadPool.submit(() -> {
                    boolean b = logSqlService.addSqlLog(logSql);
                    if (!b) {
                        throw new RuntimeException();
                    }
                });
                submit.get();
            } catch (Exception e) {
                logger.error("referent log service fail!");
            }
        }
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);// It is the point
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 返回四种语句中 第一个出现的语句的 字符串索引下标 全找不到返回-1
     *
     * @param sql sql
     * @return index or -1
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        List<Integer> list = new ArrayList<>();
        int indexSelect = upperCaseSql.indexOf("SELECT ");
        if (indexSelect != -1) list.add(indexSelect);
        int indexUpdate = upperCaseSql.indexOf("UPDATE ");
        if (indexUpdate != -1) list.add(indexUpdate);
        int indexInsert = upperCaseSql.indexOf("INSERT ");
        if (indexInsert != -1) list.add(indexInsert);
        int indexDelete = upperCaseSql.indexOf("DELETE ");
        if (indexDelete != -1) list.add(indexDelete);
        if (list.size() < 1) {
            return -1;
        }
        list.sort(Integer::compareTo);
        return list.get(0);
    }

    private String getOriginalSql() {
        // Get statement class name
        String stmtClassName = this.statement.getClass().getName();
        Class<?> clazz;
        String originalSql = null;
        if ("com.alibaba.druid.pool.DruidPooledPreparedStatement".equals(stmtClassName)) {
            try {
                if (this.druidGetSQLMethod == null) {
                    clazz = Class.forName(stmtClassName);
                    this.druidGetSQLMethod = clazz.getMethod("getSql");
                }
                Object stmtSql = this.druidGetSQLMethod.invoke(this.statement);
                if (stmtSql instanceof String) {
                    originalSql = (String) stmtSql;
                }
            } catch (Exception e) {
                logger.error("Get sql fail from {} ", stmtClassName);
            }
        } else if ("oracle.jdbc.driver.T4CPreparedStatement".equals(stmtClassName) || "oracle.jdbc.driver.OraclePreparedStatementWrapper".equals(stmtClassName)) {
            try {
                if (this.oracleGetOriginalSqlMethod == null) {
                    clazz = Class.forName(stmtClassName);
                    this.oracleGetOriginalSqlMethod = ReflectUtil.getMethodRegular(clazz, "getOriginalSql");
                    if (this.oracleGetOriginalSqlMethod != null) {
                        this.oracleGetOriginalSqlMethod.setAccessible(true);
                    }
                }
                if (this.oracleGetOriginalSqlMethod != null) {
                    Object stmtSql = this.oracleGetOriginalSqlMethod.invoke(this.statement);
                    if (stmtSql instanceof String) {
                        originalSql = (String) stmtSql;
                    }
                }
            } catch (Exception e) {
                logger.error("Get sql fail from {} ", stmtClassName);
            }
        }
        return originalSql;
    }

    /**
     * 获取statement的id
     *
     * @param invocation invocation
     * @return id
     */
    private String getStatementId(Invocation invocation) {
        Object target = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(target);
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        return ms.getId();
    }
}
